require 'rspec/core/rake_task'
require 'stove/rake_task'
require_relative 'tasks/maintainers'
require 'fileutils'

RSpec::Core::RakeTask.new(:spec) do |t|
  t.pattern = FileList['spec/**/*_spec.rb']
end

Stove::RakeTask.new

task default: :spec

#
# "rake update" updates the copied_from_chef files so we can grab bugfixes or new features
#
GLOBAL_MONKEYPATCHES = %w(
                chef/runner.rb
                chef/run_context.rb
                chef/recipe.rb
)

CHEF_FILES = %w(
                chef/constants.rb
                chef/delayed_evaluator.rb
                chef/dsl/core.rb
                chef/dsl/declare_resource.rb
                chef/dsl/platform_introspection.rb
                chef/dsl/recipe.rb
                chef/dsl/universal.rb
                chef/mixin/lazy_module_include.rb
                chef/mixin/notifying_block.rb
                chef/mixin/params_validate.rb
                chef/mixin/powershell_out.rb
                chef/mixin/properties.rb
                chef/property.rb
                chef/provider.rb
                chef/provider/support/yum_repo.erb
                chef/provider/apt_update.rb
                chef/provider/apt_repository.rb
                chef/provider/noop.rb
                chef/provider/systemd_unit.rb
                chef/provider/yum_repository.rb
                chef/resource.rb
                chef/resource/action_class.rb
                chef/resource/apt_update.rb
                chef/resource/apt_repository.rb
                chef/resource/systemd_unit.rb
                chef/resource/yum_repository.rb
                chef/resource_builder.rb
)
KEEP_FUNCTIONS = {
  'chef/resource.rb' => %w(
    initialize

    name

    resource_name self.use_automatic_resource_name

    identity state state_for_resource_reporter property_is_set reset_property
    resource_initializing resource_initializing= to_hash
    self.properties self.state_properties self.state_attr
    self.identity_properties self.identity_property self.identity_attrs
    self.property self.property_type
    self.lazy

    action allowed_actions self.allowed_actions self.default_action
    self.action self.declare_action_class self.action_class

    load_current_value current_value_does_not_exist
    self.load_current_value
  ),
    'chef/provider.rb' => %w(
    initialize
    converge_if_changed
    compile_and_converge_action
    action
    self.use_inline_resources
    self.include_resource_dsl
    self.include_resource_dsl_module
  ),
    'chef/dsl/recipe.rb' => %w(),
}
KEEP_INCLUDES = {
  'chef/resource.rb' => %w(Chef::Mixin::ParamsValidate Chef::Mixin::Properties),
  'chef/provider.rb' => %w(Chef::DSL::Core),
  'chef/dsl/recipe.rb' => %w(Chef::DSL::Core Chef::DSL::Recipe Chef::Mixin::LazyModuleInclude),
}
KEEP_CLASSES = {
  'chef/provider.rb' => %w(Chef::Provider Chef::Provider::InlineResources Chef::Provider::InlineResources::ClassMethods)
}
SKIP_LINES = {
  'chef/dsl/recipe.rb' => [ /include Chef::Mixin::PowershellOut/ ]
}
PROCESS_LINES = {
}
# See chef_compat/resource for def. of resource_name and provider
# See chef_compat/monkeypatches/chef/resource for def. of current_value

desc "Pull new files from the chef client this is bundled with and update this cookbook"
task :update do
  # Copy files from chef to chef_compat/chef, with a few changes
  target_path = File.expand_path("../files/lib/chef_compat/copied_from_chef", __FILE__)
  chef_gem_path = Bundler.environment.specs['chef'].first.full_gem_path
  CHEF_FILES.each do |file|
    if file =~ /\.rb$/
      output = StringIO.new
      output.puts "#"
      output.puts "# NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE"
      output.puts "#"
      output.puts "# THIS IS A FILE AUTOGENERATED BY 'rake update' DO NOT EDIT!!!!"
      output.puts "#"
      output.puts "# NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE"
      output.puts "#"
      output.puts ""
      # First lets try to load the original file if it exists
      output.puts "begin"
      output.puts "  require '#{file.sub(/.rb$/,"")}'"
      output.puts "rescue LoadError; end"
      output.puts ""
      # Wrap the whole thing in a ChefCompat module
      output.puts "require 'chef_compat/copied_from_chef'"
      output.puts "class Chef"
      output.puts "module ::ChefCompat"
      output.puts "module CopiedFromChef"

      # Bring over the Chef file
      chef_contents = IO.read(File.join(chef_gem_path, 'lib', "#{file}"))
      skip_until = nil
      keep_until = nil
      in_class = []
      chef_contents.lines.each do |line|
        if keep_until
          keep_until = nil if keep_until === line
        else

          if skip_until
            skip_until = nil if skip_until === line
            next
          end

          # If this file only keeps certain functions, detect which function we are
          # in and only keep those. Also strip comments outside functions

          case line

            # Skip modules and classes that aren't part of our list
          when /\A(\s*)def\s+([A-Za-z0-9_.]+)/
            if KEEP_FUNCTIONS[file] && !KEEP_FUNCTIONS[file].include?($2)
              skip_until = /\A#{$1}end\s*$/
              next
            else
              function = $2
              # Keep everything inside a function no matter what it is
              keep_until = /\A#{$1}end\s*$/
            end

            # Skip comments and whitespace if we're narrowing the file (otherwise it
            # looks super weird)
          when /\A\s*#/, /\A\s*$/
            next if KEEP_CLASSES[file] || KEEP_FUNCTIONS[file]

            # Skip aliases/attrs/properties that we're not keeping
          when /\A\s*(attr_reader|attr_writer|attr_accessor|property|alias)\s*:(\w+)/
            next if KEEP_FUNCTIONS[file] && !KEEP_FUNCTIONS[file].include?($2)

            # Skip includes and extends that we're not keeping
          when /\A\s*(include|extend)\s*([A-Za-z0-9_:]+)/
            next if KEEP_INCLUDES[file] && !KEEP_INCLUDES[file].include?($2)

          end

          next if SKIP_LINES[file] && SKIP_LINES[file].any? { |skip| skip === line }
        end

        # If we are at the end of a class, pop in_class
        if in_class[-1] && in_class[-1][:until].match(line)
          class_name = in_class.pop[:name]
          # Don't bother printing classes/modules that we're not going to print anything under
          next if KEEP_CLASSES[file] && !KEEP_CLASSES[file].any? { |c| c.start_with?(class_name) }

          # Detect class open
        elsif line =~ /\A(\s*)(class|module)(\s+)([A-Za-z0-9_:]+)(\s*<\s*([A-Za-z0-9_:]+))?.*$/
          indent, type, space, class_name, _, superclass_name = $1, $2, $3, $4, $5, $6
          full_class_name = in_class[-1] ? "#{in_class[-1][:name]}::#{class_name}" : class_name
          in_class << { name: full_class_name, until: /\A#{indent}end\s*$/ }
          superclass_name ||= "Object"

          # Don't print the class open unless it contains stuff we'll keep
          next if KEEP_CLASSES[file] && !KEEP_CLASSES[file].any? { |c| c.start_with?(full_class_name) }

          # Fix the class to extend from its parent
          original_class = "::#{full_class_name}"
          if type == 'class'
            line = "#{indent}#{type}#{space}#{class_name} < (defined?(#{original_class}) ? #{original_class} : #{superclass_name})"
          else
            # Modules have a harder time of it because of self methods
            line += "#{indent}  CopiedFromChef.extend_chef_module(#{original_class}, self) if defined?(#{original_class})"
          end

          # If we're not in a class we care about, don't print stuff
        elsif KEEP_CLASSES[file] && in_class[-1] && !KEEP_CLASSES[file].any? { |c| c == in_class[-1][:name] }
          next
        end

        # Modify requires to overridden files to bring in the local version
        if line =~ /\A(\s*require\s*['"])([^'"]+)(['"].*)/
          if CHEF_FILES.include?("#{$2}.rb")
            line = "#{$1}chef_compat/copied_from_chef/#{$2}#{$3}"
          else
            next
          end
        end

        line = PROCESS_LINES[file].call(line) if PROCESS_LINES[file]

        output.puts line

        # If this was the header for an initialize function, write out "super"
        if function == 'initialize'
          output.puts "super if defined?(::#{in_class[-1][:name]})"
        end
      end
      # Close the ChefCompat module declaration from the top
      output.puts "end"
      output.puts "end"
      output.puts "end"

      # Write out the file in chef_compat
      target_file = File.join(target_path, "#{file}")
      if !File.exist?(target_file) || IO.read(target_file) != output.string
        puts "Writing #{target_file} ..."
        FileUtils.mkdir_p(File.dirname(target_file))
        File.open(target_file, "w") { |f| f.write(output.string) }
      end
    else  # non-rb file so no mangling
      target_file = File.join(target_path, "#{file}")
      source_file = File.join(chef_gem_path, 'lib', "#{file}")
      if !File.exist?(target_file) || IO.read(target_file) != IO.read(source_file)
        puts "Writing #{target_file} ..."
        FileUtils.mkdir_p(File.dirname(target_file))
        FileUtils.cp File.join(chef_gem_path, 'lib', "#{file}"), target_file
      end
    end
  end

  target_path = File.expand_path("../files/lib/chef_compat/monkeypatches", __FILE__)

  chef_gemspec = Gem::Specification.find_by_name("chef")

  GLOBAL_MONKEYPATCHES.each do |file|
    output = StringIO.new
    output.puts "#"
    output.puts "# NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE"
    output.puts "#"
    output.puts "# THIS IS A FILE AUTOGENERATED BY 'rake update' DO NOT EDIT!!!!"
    output.puts "#"
    output.puts "# NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE"
    output.puts "#"
    output.puts ""
    output.puts "if Gem::Requirement.new('< #{chef_gemspec.version}').satisfied_by?(Gem::Version.new(Chef::VERSION))"
    chef_contents = IO.read(File.join(chef_gem_path, 'lib', "#{file}"))
    chef_contents.lines.each do |line|
      # we're probably going to want to mangle this stuff at some point i suspect
      output.puts line
    end
    output.puts "end"

    # Write out the file in chef_compat
    target_file = File.join(target_path, "#{file}")
    if !File.exist?(target_file) || IO.read(target_file) != output.string
      puts "Writing #{target_file} ..."
      FileUtils.mkdir_p(File.dirname(target_file))
      File.open(target_file, "w") { |f| f.write(output.string) }
    end
  end

  # spit out the version somewhere we can easily slurp it up from
  File.open(File.expand_path("files/lib/chef_upstream_version.rb", File.dirname(__FILE__)), "w") do |f|
    f.write <<-EOF
        module ChefCompat
          CHEF_UPSTREAM_VERSION="#{chef_gemspec.version}"
        end
    EOF
  end
end
